﻿using System;
using System.Linq;
using System.Diagnostics;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Reflection;

namespace Landscape
{
    /// <summary>
    /// The main class of Landscape, takes care of XSL rendering operations
    /// </summary>
    public class Renderer {

        #region Configurable properties

        /// <summary>
        /// Temporary setting which change at every global rendering operation
        /// </summary>
        public static Dictionary<string, string> SessionSettings = new Dictionary<string, string>();
        #endregion

        #region Internal properties


        /// <summary>
        /// The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result
        /// </summary>
        internal static System.Web.UI.WebControls.PlaceHolder placeholder;
        internal static Dictionary<string, Elements.Control> PageControls = new Dictionary<string, Elements.Control>();

        #endregion

        private Renderer()
        {
        }

        #region Public render methods
        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XmlDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">An object which can be converted into a valid XPathDocument</param>
        /// <returns>The rendered content</returns>
        public static string Render(string templateName, object source) {
            return Renderer.Render(templateName, source, null);
        }
        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XmlDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">An object which can be converted into a valid XPathDocument</param>
        /// <param name="templateParams">A dictionary of optional params to supply to the template</param>
        /// <returns>The rendered content</returns>
        public static string Render(string templateName, object source, IDictionary<string, object> templateParams) {
            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            string result = Render(templateName, xdoc, templateParams);

            // reset rendering session settings
            // this has to be done only on public Render methods in order for it to be executed only once
            SessionSettings = new Dictionary<string, string>();

            return result;
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Data.DataSet instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">A valid System.Data.DataSet instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <returns>The rendered content</returns>
        public static void RenderToPlaceHolder(string templateName, object source, System.Web.UI.WebControls.PlaceHolder contentPlaceholder) {
            Renderer.RenderToPlaceHolder(templateName, source, contentPlaceholder, null);
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Data.DataSet instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">A valid System.Data.DataSet instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <param name="templateParams">The parameters to supply to the selected template</param> 
        /// <returns>The rendered content</returns>
        public static void RenderToPlaceHolder(string templateName, object source, System.Web.UI.WebControls.PlaceHolder contentPlaceholder, IDictionary<string, object> templateParams)
        {
            placeholder = contentPlaceholder;

            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            string result = Render(templateName, xdoc, templateParams);

            result = result.Replace("xmlns:asp=\"remove\"", "");


            placeholder.Controls.Add(placeholder.Page.ParseControl(result));
            System.Web.UI.Control parsedControl = placeholder.Controls[0];

            // process user controls
            if (PageControls.Count > 0)
            {
                foreach (Elements.Control control in PageControls.Values)
                {
                    System.Web.UI.WebControls.PlaceHolder ucPlaceholder = (System.Web.UI.WebControls.PlaceHolder)Globals.FindControlRecursive(parsedControl, control.PlaceHolderId);

                    if (placeholder.FindControl(control.PlaceHolderId) != null) {
                        System.Web.UI.Control uc = Renderer.placeholder.Page.TemplateControl.LoadControl(control.VirtualPath);
                        uc.ID = control.ControlId;

                        if (control.Properties.Count > 0)
                        {
                            foreach (KeyValuePair<string, string> param in control.Params) {
                                // parameter has a corresponding set property
                                if (control.Properties[param.Key] != null && param.Value.Length > 0)
                                {
                                    string propertyType = control.Properties[param.Key];
                                    object propertyValue = null;

                                    switch (propertyType) { 
                                        case "System.Int32":
                                            propertyValue = Convert.ToInt32(param.Value);
                                            break;
                                        case "System.String":
                                            propertyValue = param.Value;
                                            break;
                                        case "System.Boolean":
                                            int propertyTempValue = Convert.ToInt32(param.Value);
                                            propertyValue = propertyTempValue == 1 ? true : false;
                                            break;
                                    }

                                    // dynamically set object property
                                    if (propertyValue != null) {
                                        uc.GetType().GetProperty(param.Key).SetValue(uc, propertyValue, null);                                    
                                    }
                                }
                            }
                        }

                        ucPlaceholder.Controls.Add(uc);
                    }
                }
            }            

            // reset rendering session settings
            // this has to be done only on public Render methods in order for it to be executed only once
            SessionSettings = new Dictionary<string, string>();
        }
        /// <summary>
        /// Renders a widget using the specified parameters
        /// </summary>
        /// <param name="name">The name of the widgets</param>
        /// <param name="source">An object which can be converted into an XPathDocument</param>
        /// <param name="content">The widget content.</param>
        /// <param name="parameters">A dictionary of string parameters supplied to the widget</param>
        /// <returns>The rendered string</returns>
        public static string RenderWidget(string name, object source, string content, Dictionary<string, object> parameters) {
            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            Elements.Widget w = new Elements.Widget() { Content = content, Params = parameters, Name = name };

            string widgetFile = w.Name + Context.xslExtension;
            string widgetPath = Globals.LookupFile(widgetFile, Context.Library.widgetPaths);

            if (string.IsNullOrEmpty(widgetPath)) {
                throw new Exception("Widget not found after lookup in " + Context.Library.widgetPaths.Count + " dirs: " + widgetFile);
            }

            WidgetTemplate template = WidgetTemplate.GetTemplate(widgetPath);

            string result = null;
            using (Stream ms = Context.Library.compiler.Transform(xdoc, template.TemplateCompiledTransform, w.Params)) {
                using (StreamReader sr = new StreamReader(ms)) {
                    result = sr.ReadToEnd();
                }
            }
            return result;
        }

        #endregion

        #region Actual render methods
        /// <summary>
        /// Renders the specified XSLT template using a valid XPath Document
        /// </summary>
        /// <param name="templatePath">The physical path to the XSLT template</param>
        /// <param name="xdoc">The Source Document</param>
        /// <param name="placeholderContent">An optional content for the wrapper</param>
        /// <param name="templateParams">A dictionary of params supplied to the template</param>
        /// <param name="wrapperName">The name of the wrapper of this page, if any</param>
        /// <param name="wrapperParams">A dictionary of params supplied to the wrapper, if any</param>
        /// <returns>The rendered content</returns>
        private static string RenderTemplate(string templatePath, 
                                             XPathDocument xdoc, 
                                             string placeholderContent, 
                                             IDictionary<string, object> templateParams, 
                                             out string wrapperName, 
                                             out IDictionary<string, object> wrapperParams) {
            wrapperName = null;
            wrapperParams = null;

            DocumentTemplate template = DocumentTemplate.GetTemplate(templatePath);

            // performs current template transformation
            string resultXml = null;
            using (Stream ms = Context.Library.compiler.Transform(xdoc, template.TemplateCompiledTransform, templateParams)) {
                using (StreamReader sr = new StreamReader(ms)) {
                    resultXml = sr.ReadToEnd();
                }
            }

            // performs master transformation and return result
            System.Xml.ConformanceLevel resultConformance;
            if (string.IsNullOrEmpty(template.WrapperName)) {
                resultConformance = ConformanceLevel.Document;
                // FIXME or wait for Xhtml output method implementation (SAXON 2, libxslt :()
                // fix short closing tags
                template.OutputMethod = OutputMethods.XHtml;
                if (template.OutputMethod == OutputMethods.XHtml) {
                    resultXml = Regex.Replace(resultXml, "<(?<tag>div|span|p|script|style|title|select|option|a|ul|li)(?<attrs> [^>]*)?/>", "<${tag}${attrs}></${tag}>");
                }
            } else {
                resultConformance = ConformanceLevel.Fragment;
                wrapperName = template.WrapperName;
                wrapperParams = ParseWrapperParameters(ref resultXml);
            }

            XmlDocument resultDoc = new XmlDocument();
            // disable resolving references to external resources such as DTDs, entities, and schemas. 
            resultDoc.XmlResolver = null;

            if (resultConformance == ConformanceLevel.Document) {
                resultDoc.LoadXml(resultXml);
            } else {
                resultDoc.LoadXml("<root/>");
                resultDoc.DocumentElement.InnerXml = resultXml;
            }
            

            // ancestor templates processing
            if (placeholderContent != null) {
                ReplacePlaceholder(ref resultDoc, placeholderContent);
            }

            // process widgets and user controls
            ReplaceWidgets(ref resultDoc, xdoc);
            ParseControls(ref resultDoc, xdoc);

            return resultConformance == ConformanceLevel.Document ? resultDoc.OuterXml : resultDoc.DocumentElement.InnerXml;
        }

        /// <summary>
        /// Starts a rendering operation
        /// </summary>
        /// <param name="templateName">The template where to start render</param>
        /// <param name="xmldom">The source document</param>
        /// <param name="templateParams">A dictionary of parameters supplied to the template</param>
        /// <returns>The rendered content</returns>
        private static string Render(string templateName, XPathDocument xmldom, IDictionary<string, object> templateParams) {
            string procTemplate = templateName;
            IDictionary<string, object> procParams = templateParams;
            int procOrder = 0;
            string buffer = null;

            Landscape.Diagnostics.RenderSession diag = new Landscape.Diagnostics.RenderSession();
            DateTime timerOn = DateTime.Now;
            while (!string.IsNullOrEmpty(procTemplate)) {
                string templateFile = procTemplate + Context.xslExtension;
                string templatePath = Globals.LookupFile(templateFile, Context.Library.templatePaths);
                DateTime timerTaskOn;
                TimeSpan taskElapsed;

                if (!System.IO.File.Exists(templatePath)) {
                    throw new TemplateNotFoundException(templatePath, Context.Library.templatePaths.Values.ToArray(), String.Format("Cannot find template {0} after lookup in {1} directories", templateName, Context.Library.templatePaths.Count.ToString()));
                }

                try {
                    timerTaskOn = DateTime.Now;
                    buffer = RenderTemplate(templatePath, xmldom, buffer, procParams, out procTemplate, out procParams);
                    taskElapsed = (DateTime.Now - timerTaskOn);
                    diag.Tasks.Add(new Diagnostics.RenderTask() { Duration = taskElapsed, TemplatePath = templatePath, Order = procOrder });
                    procOrder++;
                } catch (Exception ex) {
                    throw new RenderException(templatePath, String.Format("An error occurred while rendering template: {0} at {1}", ex.Message, templatePath), ex);
                }
            }
            diag.Duration = DateTime.Now - timerOn;
            if (HttpContext.Current != null && HttpContext.Current.Session != null) {
                HttpContext.Current.Session.Add("rendererlastsession", diag);            
            }

            return buffer;
        }
        #endregion

        /// <summary>
        /// Transforms an XPath Document using the specified compiled stylesheet
        /// </summary>
        /// <param name="xmldoc">The source document</param>
        /// <param name="compiledTransform">A compiled version of the xsl template</param>
        /// <param name="xslParams">A dictionary of parameters supplied to the template</param>
        /// <returns>The transformed content</returns>
        public static Stream XslTransform(XPathDocument xmldoc, XslCompiledTransform compiledTransform, IDictionary<string, object> xslParams)
        {
            XsltArgumentList xsltArgs = new XsltArgumentList();
            xsltArgs.AddExtensionObject("urn:landscape", new XSLT.Library(ref SessionSettings));

            Dictionary<string, object> xsltExtensions = new Dictionary<string, object>(Renderer.XSLTExtensions);

            if (xsltExtensions.Count > 0) {
                foreach (KeyValuePair<string, object> ext in xsltExtensions) {
                    xsltArgs.AddExtensionObject(ext.Key, ext.Value);
                }
            }

            // add specified params
            if(xslParams != null && xslParams.Count > 0){
                foreach (KeyValuePair<string, object> xslParam in xslParams) {
                    xsltArgs.AddParam(xslParam.Key, "", xslParam.Value);
                }
            }


            MemoryStream ms = new MemoryStream();
            compiledTransform.Transform(xmldoc, xsltArgs, ms);
            ms.Position = 0;

            return ms;
        }

        /// <summary>
        /// Gets core extensions along with all extensions defined into configuration
        /// </summary>
        /// <returns></returns>
        internal static Dictionary<string, object> _XSLTExtensions;
        internal static Dictionary<string, object> XSLTExtensions {
            get{
                if(_XSLTExtensions == null){
                    _XSLTExtensions = new Dictionary<string, object>();

                    if (Context.Library.xsltExtensions != null && Context.Library.xsltExtensions.Count > 0) {
                        foreach (string alias in Context.Library.xsltExtensions.Keys) {
                            Members.ExtensionObject extensionObject = Members.ExtensionObject.GetExtensionObject(alias, Context.Library.xsltExtensions[alias]);

                            _XSLTExtensions.Add(extensionObject.Alias, extensionObject.Extension);
                        }
                    }                
                }

                return _XSLTExtensions;
            }
        }

        private static XmlAttribute SetAttribute(XmlDocument doc, XmlElement element, string name, string value)
        {
            XmlAttribute attr = doc.CreateAttribute(name);
            attr.Value = value;
            element.Attributes.Append(attr);

            return attr;
        }

        private static XmlNode CreateDocumentFragment(XmlDocument doc, string content)
        {
            XmlDocumentFragment fragment = doc.CreateDocumentFragment();
            fragment.InnerXml = content;
            return fragment;
        }

        internal static void CreateXslOutput(XmlDocument doc, OutputMethods method, string docTypePublic, string docTypeSystem, string encoding, bool omitXmlDeclaration, bool standalone, bool indent, bool force)
        {
            XmlElement outputTag = doc.CreateElement("xsl", "output", Context.xslNamespace);
            SetAttribute(doc, outputTag, "version", Context.xslVersion);
            SetAttribute(doc, outputTag, "method", Enum.GetName(typeof(OutputMethods), method).ToLower());
            //SetAttribute(doc, outputTag, "encoding", encoding);
            SetAttribute(doc, outputTag, "omit-xml-declaration", (omitXmlDeclaration == true ? "yes" : "no"));
            SetAttribute(doc, outputTag, "standalone", (standalone == true ? "yes" : "no"));
            SetAttribute(doc, outputTag, "indent", (indent == true ? "yes" : "no"));
            if (!string.IsNullOrEmpty(docTypePublic)) {
                SetAttribute(doc, outputTag, "doctype-public", docTypePublic);
            }

            if (!string.IsNullOrEmpty(docTypeSystem)){
                SetAttribute(doc, outputTag, "doctype-system", docTypeSystem);
            }

            XmlNodeList xslOutputTags = doc.GetElementsByTagName("output", Context.xslNamespace);
            if (xslOutputTags.Count > 0)
            {
                XmlNode oldOutputTag = xslOutputTags.Item(0);

                if (force == true)
                {
                    doc.DocumentElement.ReplaceChild(outputTag, oldOutputTag);
                }
            }else {
                XmlNode xslOutputLastPrecedingSibling = doc.DocumentElement.FirstChild;

                // insert just after the last import node, if any
                XmlNodeList xslImports = doc.GetElementsByTagName("import", Context.xslNamespace);
                if (xslImports.Count > 0)
                {
                    xslOutputLastPrecedingSibling = xslImports.Item(xslImports.Count - 1);
                    doc.DocumentElement.InsertAfter(outputTag, xslOutputLastPrecedingSibling);
                }
                else {
                    doc.DocumentElement.InsertBefore(outputTag, xslOutputLastPrecedingSibling);
                }
                
            }
        }

        private static void ReplacePlaceholder(ref XmlDocument xmlDocument, string xmlContent) {
            XmlNodeList pholders = xmlDocument.GetElementsByTagName("pholder", Context.lscNamespace);

            // tries to substitute placeholder with content
            // if placeholder id doesn't match, just removes it
            if (pholders.Count > 0)
            {
                XmlNode p = pholders.Item(0);

                if (p != null)
                {
                    p.ParentNode.ReplaceChild(CreateDocumentFragment(xmlDocument, xmlContent), p);
                }
                else
                {
                    p.ParentNode.RemoveChild(p);
                }
            }     
        }

        public static void LoadXPathDocument(object source, out XPathDocument xdoc) {
            if (source == null) {
                throw new ArgumentNullException("source");
            }else if (source is XPathDocument) {
                xdoc = (XPathDocument)source;
            }else if (source is XmlDocument) {
                XmlDocument doc = (XmlDocument)source;
                xdoc = new XPathDocument(new StringReader(doc.OuterXml));
            } else if (source is DataSet) {
                DataSet ds = (DataSet)source;
                using (MemoryStream dataStream = new MemoryStream()) {
                    ds.WriteXml(dataStream);
                    dataStream.Position = 0;
                    xdoc = new XPathDocument(dataStream);
                }
            }else if(source is System.IO.Stream){
                xdoc = new XPathDocument((System.IO.Stream)source);
            } else if (source is string) {
                xdoc = new XPathDocument(new StringReader((string)source));
            } else {
                string msg = string.Format("Source type '{0}' has no valid implementation", source.GetType().FullName);
                throw new InvalidSourceTypeException(source, msg);
            }
        }

        private static bool ReplaceWidgets(ref XmlDocument resultDocument, XPathDocument xmldom) {
            XmlNodeList nodes = resultDocument.GetElementsByTagName("widget", Context.lscNamespace);
            if (nodes.Count > 0)
            {

                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> widgets = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    widgets.Add(node);
                }

                foreach (XmlNode node in widgets)
                {
                    Elements.Widget w = new Elements.Widget(node);

                    string widgetFile = w.Name + Context.xslExtension;
                    string widgetPath = Globals.LookupFile(widgetFile, Context.Library.widgetPaths);

                    if (string.IsNullOrEmpty(widgetPath))
                    {
                        throw new Exception("Widget not found after lookup in " + Context.Library.widgetPaths.Count + " dirs: " + widgetFile);
                    }

                    WidgetTemplate template = WidgetTemplate.GetTemplate(widgetPath);

                    string replacement = null;
                    using (Stream ms = Context.Library.compiler.Transform(xmldom, template.TemplateCompiledTransform, w.Params)) {
                        using (StreamReader sr = new StreamReader(ms)) {
                            replacement = sr.ReadToEnd();
                        }
                    }

                    node.ParentNode.ReplaceChild(CreateDocumentFragment(resultDocument, replacement), node);
                }

                // recursively replace nested widgets
                return ReplaceWidgets(ref resultDocument, xmldom);
            } else {
                return false;
            }
        }

        private static void ParseControls(ref XmlDocument resultDocument, XPathDocument xmldom)
        {
            // don't parse controls since there is no valid source directory configured
            if (Context.Library.controlPaths.Count == 0) return;

            XmlNodeList nodes = resultDocument.GetElementsByTagName("control", Context.lscNamespace);
            if (nodes.Count > 0)
            {

                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> controls = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    controls.Add(node);
                }

                foreach (XmlNode node in controls)
                {
                    //TODO: control caching
                    Elements.Control control = new Elements.Control(node);

                    if (!PageControls.ContainsKey(control.ControlId)) {
                        PageControls.Add(control.ControlId, control);
                    }
                }
            }
        }

        private static IDictionary<string, object> ParseWrapperParameters(ref string buffer) {
            IDictionary<string, object> result = new Dictionary<string, object>();

            XmlDocument bufferDom = new XmlDocument();

            bufferDom.LoadXml("<lsc/>");
            XmlNode body = CreateDocumentFragment(bufferDom, buffer);
            bufferDom.DocumentElement.AppendChild(body);

            XmlNodeList nodes = bufferDom.GetElementsByTagName("param", Context.lscNamespace);

            if (nodes.Count > 0) {
                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> paramNodes = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    paramNodes.Add(node);
                }

                foreach(XmlNode paramNode in paramNodes){
                    Elements.Parameter parameter = new Elements.Parameter(paramNode);
                    result.Add(parameter.Name, parameter.Value);

                    paramNode.ParentNode.RemoveChild(paramNode);
                }

                buffer = bufferDom.DocumentElement.InnerXml;
            }

            return result;
        }
    }
}